力扣142——环形链表 II

这道题主要就是找规律,推导出路径之间的关系,从而解决问题。

原题

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

说明:不允许修改给定的链表。

示例 1:

1
2
3
输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

1
2
3
输入:head = [1,2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

1
2
3
输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。

进阶:

你是否可以不用额外空间解决此题?

原题url:https://leetcode-cn.com/problems/linked-list-cycle-ii/

解题

在这里贴一下题目所提供的节点结构,这样下面的代码就不重复贴了:

1
2
3
4
5
6
7
8
9
Definition for singly-linked list.
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}

利用集合

拿到题目的时候,一开始想到的就是利用集合,存储已经遍历过的节点,如果访问到 null,说明不是环;如果添加失败,说明已经添加过,那么一定是环,并且该节点就是环的入口;

顺便说一句,我认为集合所占空间应该不是很大,因为它只是存储对象的应用地址,当然了,集合本身也是一个新的对象,也会占用额外的空间。

让我们看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}

ListNode current = head;
Set<ListNode> set = new HashSet<>();
while (current != null) {
// 添加成功,则继续访问下一个节点
if (set.add(current)) {
current = current.next;
continue;
}

// 添加不成功,说明重复
return current;
}

return null;
}
}

提交OK,执行用时:5 ms,内存消耗:37.7 MB,但是提交用时只战胜了30.99%的 java 提交记录,看来有必要优化一下。

找规律

以前我们判断链表是否有环,都是通过快慢指针最终是否相等。现在的话,因为环可能并不是首尾相连,所以只找一次可能不够了,需要继续寻找规律。

我们假设一开始 slow 指针走过的路程为 x,那么 fast 指针走过的路程就为 2x,即:

1
2
s = x;
f = 2x;

如果 fast 指针最终为 null,那么说明不是环。

如果 fast、slow 指针最终指向的节点相等,说明有环,并且, fast 指针比 flow 指针多走了 n 圈环的长度,那么我们假设环的长度为 b,那么可以得出:

1
f = x + nb;

可以得出:s = nb;

以上就是最重要的结论了,slow 指针其实也已经走了 n 圈环的长度了。那么,我们再假设从 head 节点到环入口节点的长度为 a,那么从快慢指针相遇节点再走 a 步,最终会走到哪儿呢?

最终也会走到环的入口节点,因为(nb + a)可以理解为(a + nb),相当于从 head 节点出发,达到环的入口节点处,又绕环走了 n 圈,所以也会走到环的入口。所以此时我们也找到环的入口节点了。

接下来让我们看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Solution {
public ListNode detectCycle(ListNode head) {
// 先利用快慢指针,如果最终能相遇,说明有环
ListNode slow = head;
ListNode fast = head;
while (true) {
// 快指针为null,说明没有环
if (fast == null || fast.next == null) {
return null;
}

// 慢指针移动一步
slow = slow.next;
// 快指针移动两步
fast = fast.next.next;
// 快慢指针相等,说明相遇
if (fast == slow) {
break;
}
}

// 再用两个指针,一个从头结点出发,一个从相遇点出发,两个指针每次移动1步,两个指针相遇的地方为环的入口
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}

return slow;
}
}

提交OK,执行用时:1 ms,内存消耗:37.8 MB,但是提交用时只战胜了55.14%的 java 提交记录,难道还有更加高效的方法?

我找了一个执行用时 0 ms 的代码,发现就是和我这个类似的,我将它的代码再次提交后,发现和我这个提交结果一样。看来那些比我们快的算法,可能是因为提交时间比较早,测试案例并不像现在那么多,所以不必担心了。

总结

以上就是这道题目我的解答过程了,不知道大家是否理解了。这道题目不仅要利用快慢指针,还要总结规律,最终也能解决,总的来说是一道很考验逻辑思维的题目。

有兴趣的话可以访问我的博客或者关注我的公众号、头条号,说不定会有意外的惊喜。

https://death00.github.io/

公众号:健程之道

健健 wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
如果您感觉文章不错,也愿意支持一下作者的话